#include <cassert>
#include <cmath>
#include "colour.h"
#include "fractal.h"
#include "avxjob.h"
#include "cpu.h"

Fractal::Fractal(int w, int h, double centreX, double centreY, double scale, int limit, int nSamples) 
	:	w_ (w), h_ (h),
		limit_ (limit),
		centreX_ (centreX), centreY_ (centreY),
		scale_ (scale),
		samples_ (NULL),
		nSamples_ (nSamples)
{
	gradient_.push_back(Colour(0, 0, 0));
	gradient_.push_back(Colour(255, 255, 255));
	gradient_.push_back(Colour(0, 0, 0));
}

Fractal::~Fractal() {
	abort();
	if (samples_ != NULL)
		delete[] samples_;
}

/* spawns nThreads threads to generate the fractal */
void Fractal::generate(int nThreads) {
	int w = w_ * nSamples_;
	int h = h_ * nSamples_;
	double scale = scale_ / nSamples_;
	double topLeftX = centreX_ - ((w * scale) / 2);
	double topLeftY = centreY_ - ((h * scale) / 2);
	
	// abort any running threads
	abort();
	
	// allocate memory for fractal data
	if (samples_ != NULL)
		delete[] samples_;
	samples_ = new double [w * h];
	for (int i = 0; i < (w * h); i++) {
		samples_[i] = 0;
	}

	// create a new job queue
	jobQueue_.reset();
	

	// add a job for each row of the fractal
	for (int i = 0; i < h; i++) {
		if (getCpuInfo()->hasAvx)
			jobQueue_.push(new AvxJob(w, 1, topLeftX, topLeftY + (i * scale), scale, limit_, samples_ + (i * w)));
		else
			jobQueue_.push(new DoubleJob(w, 1, topLeftX, topLeftY + (i * scale), scale, limit_, samples_ + (i * w)));
	}

	// spawn threads
	for (int i = 0; i < nThreads; i++) {
		threads_.push_back(new Thread(jobQueue_));
	}
}

void Fractal::abort() {
	if (threads_.size() != 0) {
		for (unsigned int i = 0; i < threads_.size(); i++) {
			delete threads_[i];
			threads_[i] = NULL;
		}
		threads_.clear();
	}
}

Glib::RefPtr<Gdk::Pixbuf> Fractal::createPixbuf() {
	double iter;
	
	double *downSampled = downSample();
	
	/* we must first get the minimum value in order to normalise
	 * the colours. */
	double min = 1;
	for (int y = 0; y < h_; ++y) {
		for (int x = 0; x < w_; ++x) {
			if ((downSampled[(w_ * y) + x] < min))
				min = downSampled[(w_ * y) + x];
		}
	}
	
	Glib::RefPtr<Gdk::Pixbuf> pixbuf = Gdk::Pixbuf::create(Gdk::Colorspace(0), false, 8, w_, h_);
	guint8* data = pixbuf->get_pixels();
	for (int y = 0; y < h_; ++y) {
		for (int x = 0; x < w_; ++x) {
			/* normalise the colours */
			iter = downSampled[(w_ * y) + x];
			if (min < 1) {
				iter = (iter - min) * (1.0 / (1.0 - min));
			}
			Colour colour = gradient_.getColour(iter);
			data[(((w_ * 3) + (w_ % 4)) * y) + (3 * x) + 0] = colour.r;
			data[(((w_ * 3) + (w_ % 4)) * y) + (3 * x) + 1] = colour.g;
			data[(((w_ * 3) + (w_ % 4)) * y) + (3 * x) + 2] = colour.b;
		}
	}
	delete[] downSampled;
	return pixbuf;
}

int Fractal::getW() {
	return w_;
}

int Fractal::getH() {
	return h_;
}

int Fractal::getLimit() {
	return limit_;
}

double Fractal::getCentreX() {
	return centreX_;
}

double Fractal::getCentreY() {
	return centreY_;
}

double Fractal::getScale() {
	return scale_;
}

int Fractal::getNSamples() {
	return nSamples_;
}

void Fractal::setSize(int w, int h) {
	w_ = w;
	h_ = h;
}

void Fractal::setW(int w) {
	w_ = w;
}

void Fractal::setH(int h) {
	h_ = h;
}

void Fractal::setLimit(int limit) {
	limit_ = limit;	
}

void Fractal::setCentre(double x, double y) {
	centreX_ = x;
	centreY_ = y;
}

void Fractal::setCentreX(double centreX) {
	centreX_ = centreX;
}

void Fractal::setCentreY(double centreY) {
	centreY_ = centreY;
}

void Fractal::setScale(double scale) {
	scale_ = scale;
}

void Fractal::setNSamples(int nSamples) {
	nSamples_ = nSamples;
}

/* returns true if all of the Generators have finished */
bool Fractal::isComplete() {
	assert(threads_.size() > 0);
	return jobQueue_.isCompleted();
}

/* gets the progress from each Generator and returns an average */
float Fractal::getProgress() {
	assert(threads_.size() > 0);
	return jobQueue_.getCompletion();
}

Gradient &Fractal::getGradient() {
	return gradient_;
}

/* naive (mean average) downsample the fractal */
double *Fractal::downSample() {
	double *downSampled = new double [w_ * h_];
	double total;
	for (int y = 0; y < h_; ++y) {
		for (int x = 0; x < w_; ++x) {
			total = 0;
			for (int osY = y * nSamples_; osY < (y * nSamples_) + nSamples_; osY++) {
				for (int osX = x * nSamples_; osX < (x * nSamples_) + nSamples_; osX++) {
					total += samples_[(osY * w_ * nSamples_) + osX];
				}
			}
			downSampled[(w_ * y) + x] = total / (nSamples_ * nSamples_);
		}
	}
	return downSampled;
}

